package com.friddle.Download.PageGet;

import com.friddle.util.Assert;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by friddle on 12/30/14. this tragetry will contain four part!static
 * part will contains all the values the new part will must be contains the
 * Threadpool to control it
 */
public class PageGetter {
	public static ConcurrentHashMap<String, PageGetter> pageGetterList = new ConcurrentHashMap<String, PageGetter>();
	public GetConfigure configure;
	public AntiStrategy strategy;
	public String host;
	private final static Lock mutex = new ReentrantLock(true);
	private HttpClientGenerator httpClientGenerator = new HttpClientGenerator();
	private final Map<String, CloseableHttpClient> httpClients = new HashMap<String, CloseableHttpClient>();
    private final Map<String, CookieStore> stores=new HashMap<>();
	Logger logger = LoggerFactory.getLogger(this.getClass());

	/**
	 * @param url
	 * @param configure
	 * @return all is default!,configure must contains the neccessary things no
	 *         lock no thread and just connnect
	 */
	public static PageResult getPageContent(String url, GetConfigure configure) {
		if (configure == null) {
			configure = new GetConfigure();
		}
		DefaultStrategy stragery = new DefaultStrategy(configure);
		PageGetter page = new PageGetter(configure, stragery);
		return page.getPageContent(url);
	}

	public static PageResult getPagePostContent(String url, List<NameValuePair> params, GetConfigure configure) {
		DefaultStrategy stragery = new DefaultStrategy(configure);
		PageGetter page = new PageGetter(configure, stragery);
		return page.getPagePostContent(url, params);
	}

	private PageGetter(GetConfigure configure, AntiStrategy stragery) {
		this.strategy = stragery;
		this.configure = configure;
	}

	public static PageGetter getPageGetterInstance(String domain) {
		GetConfigure mConfigure = new GetConfigure(domain);
		DefaultStrategy mGery = new DefaultStrategy(mConfigure);
		return getPageGetterInstance(domain, mConfigure, mGery);
	}

	public static PageGetter getPageGetterInstance() {
		return getPageGetterInstance("default");
	}

	public static PageGetter getPageGetterInstance(String domain, GetConfigure configure, AntiStrategy stradge) {
		if (domain == null) {
			domain = "default";
		}
		if (configure == null) {
			configure = new GetConfigure(domain);
		}
		if (stradge == null) {
			stradge = new DefaultStrategy(configure);
		}
		if (pageGetterList.get(domain) == null) {
			pageGetterList.put(domain, new PageGetter(configure, stradge));
		} else if (!configure.equals(configure) || !stradge.equals(stradge)) {
			pageGetterList.put(domain, new PageGetter(configure, stradge));
		}
		return pageGetterList.get(domain);
	}

	public PageResult getPageContent(String url) {
		if (!url.contains("http")) {
			url = "http://" + url;
		}
		Assert.assertNotNullException(url);
		Assert.checkUrl(url);
		return this.getContent(RequestBuilder.get().setUri(url));
	}

	public PageResult getPagePostContent(String url, List<NameValuePair> params) {
		Assert.assertNotNullException(params);
		Assert.assertNotNullException(url);
		Assert.checkUrl(url);
		RequestBuilder requestBuilder = RequestBuilder.post();
		if (params.size() > 0) {
			requestBuilder.addParameters(params.toArray(new NameValuePair[] {}));
		}
		return this.getContent(RequestBuilder.post().setUri(url));
	}

	public PageResult getPageByOtherBuilder(String url, RequestBuilder builder) {
		return this.getContent(builder.setUri(url));
	}

	private synchronized PageResult getContent(RequestBuilder builder) {
		PageResult result = this.download(configure, strategy, builder);
		int trytimes = 0;
		try {
			while (result.isNeedretry() == true && trytimes <= configure.getRetryTimes()) {
				trytimes++;
				result.setRetryTimes(trytimes);
				result = this.download(configure, strategy, builder);
				Thread.sleep(strategy.getSleepTime());
			}
			Thread.sleep(strategy.getSleepTime());
		} catch (Exception e) {
			logger.error("can't get content ", e);
		}
		return result;
	}

	public GetConfigure setDomain(GetConfigure configure, RequestBuilder builder) {
		configure.setDomain(builder.getUri().getHost());
		return configure;
	}

	/**
	 * all the configure contains must be first the --->stragery when use define
	 * the stragetry. prioperity change the configure
	 */
	protected PageResult download(GetConfigure configure, AntiStrategy strategy, RequestBuilder request) {
        if(configure.getDomain()==null)
        {
            configure = this.setDomain(configure, request);
        }
		Set<Integer> acceptStatCode;
		acceptStatCode = configure.getAcceptStatCode();
		Map<String, String> headers = strategy.getHttpHeaders();
		CloseableHttpResponse httpResponse = null;
		HttpUriRequest httpUriRequest = getHttpUriRequest(configure, request, headers, strategy.getHttpProxys());
		int statusCode;
		try {
			PageResult page;
			HttpContext context = new BasicHttpContext();
            context.setAttribute(HttpClientContext.COOKIE_STORE,stores.get(configure.getDomain()));
			CloseableHttpClient client = getHttpClient(configure, strategy);
			httpResponse = client.execute(httpUriRequest, context);
			statusCode = httpResponse.getStatusLine().getStatusCode();
			if (statusAccept(acceptStatCode, statusCode)) {
				page = handleResponse(httpUriRequest.getURI().toASCIIString(), configure, httpResponse, context);
			} else {
				page = handleErrorResponse(httpUriRequest.getURI().toASCIIString(), configure, httpResponse, context);
			}
			page = this.dealBlock(page);
			return page;
		} catch (IOException e) {
			PageResult result = handleErrorResponse(httpUriRequest.getURI().toASCIIString(), configure, e, strategy.getHttpProxys());
			result = this.dealBlock(result);
			return result;
		} finally {
			try {
				if (httpResponse != null) {
					// ensure the connection is released back to pool
					EntityUtils.consume(httpResponse.getEntity());
				}
			} catch (IOException e) {
				logger.warn("close response fail", e);
			}
		}
	}

	public PageResult dealBlock(PageResult page) {
		Map.Entry<Boolean, String> isblock = strategy.isBlock(page);
		if (isblock.getKey() == true) {
			httpClients.remove(page.getConfigure().getDomain());
			mutex.lock();
			boolean success = strategy.changeTradegy(isblock.getValue());
			if (success) {
				page.setNeedretry(true);
				page.setSuccess(false);
			} else {
				page.setNeedretry(false);
				page.setSuccess(false);
			}
			mutex.unlock();
		}
		return page;
	}

	protected String getHtmlContent(String charset, HttpResponse httpResponse) throws IOException {
		if (charset == null) {
			byte[] contentBytes = IOUtils.toByteArray(httpResponse.getEntity().getContent());
			String htmlCharset = getHtmlCharset(httpResponse, contentBytes);
			if (htmlCharset != null && !htmlCharset.contains("x-gbk")) {
				return new String(contentBytes, htmlCharset);
			} else {
				logger.warn("Charset autodetect failed, use {} as charset. Please specify charset in Site.setCharset()"
						+ Charset.defaultCharset());
				return new String(contentBytes);
			}
		} else {
			return IOUtils.toString(httpResponse.getEntity().getContent(), charset);
		}
	}

	protected String getHtmlCharset(HttpResponse httpResponse, byte[] contentBytes) throws IOException {
		String charset;
		// charset
		// 1、encoding in http header Content-Type
		String value = httpResponse.getEntity().getContentType().getValue();
		charset = UrlUtils.getCharset(value);
		if (StringUtils.isNotBlank(charset)) {
			logger.debug("Auto get charset: {}", charset);
			return charset;
		}
		// use default charset to decode first time
		Charset defaultCharset = Charset.defaultCharset();
		String content = new String(contentBytes, defaultCharset.name());
		// 2、charset in meta
		if (StringUtils.isNotEmpty(content)) {
			Document document = Jsoup.parse(content);
			Elements links = document.select("meta");
			for (Element link : links) {
				// 2.1、html4.01 <meta http-equiv="Content-Type"
				// content="text/html; charset=UTF-8" />
				String metaContent = link.attr("content");
				String metaCharset = link.attr("charset");
				if (metaContent.indexOf("charset") != -1) {
					metaContent = metaContent.substring(metaContent.indexOf("charset"), metaContent.length());
					charset = metaContent.split("=")[1];
					break;
				}
				// 2.2、html5 <meta charset="UTF-8" />
				else if (StringUtils.isNotEmpty(metaCharset)) {
					charset = metaCharset;
					break;
				}
			}
		}
		logger.debug("Auto get charset: {}", charset);
		return charset;
	}

	protected boolean statusAccept(Set<Integer> acceptStatCode, int statusCode) {
		return acceptStatCode.contains(statusCode);
	}

	protected HttpUriRequest getHttpUriRequest(GetConfigure configure, RequestBuilder requestBuilder, Map<String, String> headers,
			HttpHost proxy) {
		if (headers != null) {
			for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
				requestBuilder.addHeader(headerEntry.getKey(), headerEntry.getValue());
			}
		}
		RequestConfig.Builder requestConfigBuilder = RequestConfig.custom().setConnectionRequestTimeout(configure.getTimeOut())
				.setSocketTimeout(configure.getTimeOut()).setConnectTimeout(configure.getTimeOut()).setCookieSpec(CookieSpecs.BROWSER_COMPATIBILITY);
		if (proxy != null) {
			requestConfigBuilder.setProxy(proxy);
		}
		requestBuilder.setConfig(requestConfigBuilder.build());
		HttpUriRequest request = requestBuilder.build();
		return request;
	}

	private CloseableHttpClient getHttpClient(GetConfigure configure, AntiStrategy strategy) {
		String domain = configure.getDomain();
		CloseableHttpClient httpClient = null;
		synchronized (this) {
			httpClient = httpClients.get(domain);
			if (httpClient == null) {
				MutablePair<CloseableHttpClient,CookieStore> result= httpClientGenerator.generateClient(configure, strategy);
                httpClient=result.getLeft();
				httpClients.put(domain, httpClient);
                stores.put(domain,result.getRight());
			}
		}
        //TODO:set HttpClient dynamic
		return httpClient;
	}

    public void addCookie(String key,String value,String domain,String path)
    {
        Assert.assertNotNullException(configure.getDomain());
        CookieStore store=stores.get(configure.getDomain());
        if(store==null)
        {
            this.getHttpClient(configure,strategy);
            store=stores.get(configure.getDomain());
        }
        BasicClientCookie cookie=new BasicClientCookie(key,value);
        cookie.setDomain(domain);
        cookie.setPath(path);
        store.addCookie(cookie);
    }

	protected PageResult handleResponse(String url, GetConfigure configure, HttpResponse response, HttpContext context) throws IOException {
		PageResult mResult = new PageResult();
		mResult.setHtmlContent(getHtmlContent(configure.getCharset(), response));
		mResult.setRequestUrl(url);
		mResult.setConfigure(configure);
		mResult.setStateCode(response.getStatusLine().getStatusCode());
		mResult.setRealUrl(this.getRealUrl(context));
        mResult.setHeaders(Arrays.asList(response.getAllHeaders()));
		mResult.setSuccess(true);
		return mResult;
	}

	protected PageResult handleErrorResponse(String url, GetConfigure configure, Exception e, HttpHost proxy) {
		PageResult mResult = new PageResult();
		mResult.setErrorinfo(e.getMessage() + e.getCause());
		mResult.setProxy(proxy);
		mResult.setNeedretry(true);
		mResult.setRequestUrl(url);
		mResult.setRealUrl(url);
		mResult.setConfigure(configure);
		mResult.setSuccess(false);
		return mResult;
	}

	protected PageResult handleErrorResponse(String url, GetConfigure configure, HttpResponse response, HttpContext context) {
		PageResult mResult = new PageResult();
		try {
			mResult.setHtmlContent(getHtmlContent(configure.getCharset(), response));
		} catch (Exception e) {
			mResult.setErrorinfo(e.getMessage());
		}
		mResult.setConfigure(configure);
		mResult.setStateCode(response.getStatusLine().getStatusCode());
		mResult.setErrorinfo(mResult + ";" + "uaccept code");
		mResult.setSuccess(false);
		mResult.setRealUrl(this.getRealUrl(context));
		mResult.setRequestUrl(url);
		return mResult;
	}

	protected String getRealUrl(HttpContext context) {
		HttpUriRequest currentReq = (HttpUriRequest) context.getAttribute(ExecutionContext.HTTP_REQUEST);
		HttpHost currentHost = (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
		String currentUrl = (currentReq.getURI().isAbsolute()) ? currentReq.getURI().toString() : (currentHost.toURI() + currentReq
				.getURI());
		return currentUrl;
	}

	public static void main(String[] args) {
		String post = PageGetter.getPageContent("http://esf.hz.fang.com/house/a21/", new GetConfigure()).getHtmlContent();
		System.out.print(post);
	}

}
